﻿using System;
using System.Reflection;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using TestByWire;
using Microsoft.Practices.Unity;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Rhino.Mocks;

namespace TestByWire.Test
{
	[TestClass]
	public class ATests
	{
		private IUnityContainer _container;

		[TestInitialize]
		public void Init()
		{
			_container = new UnityContainer();
			SetScenarioRecording(true);
			A.Reset(_container);
		}

		internal static void SetScenarioRecording(bool value)
		{
			var fieldInfo = typeof(Scenario).GetField("_recordingScenario", BindingFlags.NonPublic | BindingFlags.Static);
			fieldInfo.SetValue(null, value);
		}

		[TestMethod]
		public void That_ShouldRecordVoidStubbingCorrect()
		{
			var stub = MockRepository.GenerateStub<IDbAccess>();
			_container.RegisterInstance(stub);

			A.CallTo<IDbAccess>(x => x.StoreNewSalary("foo", 123));

			var recordings = GetRecordings();

			Assert.AreEqual(1, recordings.Count);
			var invocation = recordings[0];
			var inspector = new InvocationInspector(invocation);

			Assert.IsNull(inspector.ReturnValue);
			Assert.AreEqual(-1, inspector.RepeatCount);
			Assert.IsNull(inspector.ExceptionValue);
			Assert.IsNull(inspector.Name);
		}

		[TestMethod]
		public void That_ShouldRecordNamedStubbingCorrect()
		{
			var stub = MockRepository.GenerateStub<IDbAccess>();
			_container.RegisterInstance(stub);

			A.CallTo<IDbAccess>("stub", x => x.StoreNewSalary("foo", 123));

			var recordings = GetRecordings();

			Assert.AreEqual(1, recordings.Count);
			var invocation = recordings[0];
			var inspector = new InvocationInspector(invocation);

			Assert.IsNull(inspector.ReturnValue);
			Assert.AreEqual(-1, inspector.RepeatCount);
			Assert.IsNull(inspector.ExceptionValue);
			Assert.AreEqual("stub", inspector.Name);


		}

		[TestMethod]
		public void That_ShouldTrimListCorrect_WhenDefaultIsUnlimited()
		{
			var stub = MockRepository.GenerateStub<IDbAccess>();
			_container.RegisterInstance(stub);

			A.CallTo<IDbAccess>(x => x.GetCurrentSalary("foo")).Returns(23);
			A.CallTo<IDbAccess>(x => x.GetCurrentSalary("foo")).Returns(33);

			var recordings = GetRecordings();

			Assert.AreEqual(2, recordings.Count);
			var inspector = new InvocationInspector(recordings[0]);
			var inspector2 = new InvocationInspector(recordings[1]);

			Assert.AreEqual(23, inspector.ReturnValue);
			Assert.AreEqual(1, inspector.RepeatCount);
			Assert.IsNull(inspector.ExceptionValue);

			Assert.AreEqual(33, inspector2.ReturnValue);
			Assert.AreEqual(-1, inspector2.RepeatCount);
			Assert.IsNull(inspector2.ExceptionValue);
		}

		[TestMethod]
		public void That_ShouldTrimListCorrect_WhenWhenRepeatIsUserSpecified()
		{
			var stub = MockRepository.GenerateStub<IDbAccess>();
			_container.RegisterInstance(stub);

			A.CallTo<IDbAccess>(x => x.GetCurrentSalary("foo")).Returns(23).Repeat(2);
			A.CallTo<IDbAccess>(x => x.GetCurrentSalary("foo")).Returns(33);

			var recordings = GetRecordings();

			Assert.AreEqual(2, recordings.Count);
			var inspector = new InvocationInspector(recordings[0]);
			var inspector2 = new InvocationInspector(recordings[1]);

			Assert.AreEqual(23, inspector.ReturnValue);
			Assert.AreEqual(2, inspector.RepeatCount);
			Assert.IsNull(inspector.ExceptionValue);

			Assert.AreEqual(33, inspector2.ReturnValue);
			Assert.AreEqual(-1, inspector2.RepeatCount);
			Assert.IsNull(inspector2.ExceptionValue);
		}

		[TestMethod]
		public void That_ShouldRecordThrowsStubbingCorrect()
		{
			var stub = MockRepository.GenerateStub<IDbAccess>();
			_container.RegisterInstance(stub);

			var expectedException = new Exception("Blah");
			A.CallTo<IDbAccess>(x => x.StoreNewSalary("foo", 123)).Throws(expectedException);

			var recordings = GetRecordings();

			Assert.AreEqual(1, recordings.Count);
			var invocation = recordings[0];
			var inspector = new InvocationInspector(invocation);

			Assert.IsNull(inspector.ReturnValue);
			Assert.AreEqual(-1, inspector.RepeatCount);
			Assert.AreEqual(expectedException, inspector.ExceptionValue);
		}

		[TestMethod]
		public void That_ShouldRecordReturnStubbingsCorrect()
		{
			var stub = MockRepository.GenerateStub<IDbAccess>();
			_container.RegisterInstance(stub);

			A.CallTo<IDbAccess>(x => x.GetCurrentSalary("foo")).Returns(23).Repeat(3);

			var recordings = GetRecordings();

			Assert.AreEqual(1, recordings.Count);
			var invocation = recordings[0];
			var inspector = new InvocationInspector(invocation);

			Assert.AreEqual(23, inspector.ReturnValue);
			Assert.AreEqual(3, inspector.RepeatCount);
			Assert.IsNull(inspector.ExceptionValue);
		}

		[TestMethod]
		public void ExplicitCallWithPriority()
		{
			var stub = MockRepository.GenerateStub<IDbAccess>();
			_container.RegisterInstance(stub);

			A.PriorityCallTo(stub, x => x.GetCurrentSalary("Foo"), Priority.Medium).Returns(23);

			A.Transfer();

			Assert.AreEqual(23, stub.GetCurrentSalary(null));
		}

		[TestMethod]
		public void ExplicitCallWithLowestPriority()
		{
			var stub = MockRepository.GenerateStub<IDbAccess>();
			_container.RegisterInstance(stub);

			A.PriorityCallTo(stub, x => x.GetCurrentSalary("Foo"), Priority.Low).Returns(23);
			A.CallTo<IDbAccess>(x => x.GetCurrentSalary("Bar")).Returns(30);

			A.Transfer();

			Assert.AreEqual(30, stub.GetCurrentSalary(null));
		}

		[TestMethod]
		public void POC()
		{
			var expectedException = new Exception();

			var stub = MockRepository.GenerateStub<IDbAccess>();
			_container.RegisterInstance(stub);

			A.CallTo<IDbAccess>(x => x.GetCurrentSalary("Foo")).Returns(23);
			A.CallTo<IDbAccess>(x => x.GetCurrentSalary("Foo")).Returns(30).Repeat(2);
			A.CallTo<IDbAccess>(x => x.GetCurrentSalary("Foo")).Returns(40);

			A.CallTo<IDbAccess>(x => x.StoreNewSalary("Bar", 22));
			A.CallTo<IDbAccess>(x => x.StoreNewSalary("Bar", 22)).Throws(expectedException);

			A.Transfer();

			Assert.AreEqual(23, stub.GetCurrentSalary("Foo"));
			Assert.AreEqual(30, stub.GetCurrentSalary("Foo"));
			Assert.AreEqual(30, stub.GetCurrentSalary("Foo"));
			for (int i = 0; i < 5; i++)
				Assert.AreEqual(40, stub.GetCurrentSalary("Foo"));

			stub.StoreNewSalary("Bar", 22);
			try
			{
				stub.StoreNewSalary("Bar", 22);
				Assert.Fail();
			}
			catch (Exception e)
			{
				Assert.AreEqual(expectedException, e);
			}

		}

		[TestMethod]
		public void POC_MixedWithGiven()
		{
			Given.Reset(_container);
			var stub = MockRepository.GenerateStub<IDbAccess>();
			_container.RegisterInstance(stub);

			//Greenflow
			A.CallTo<IDbAccess>(x => x.GetCurrentSalary("Foo")).Returns(23);
			A.CallTo<IDbAccess>(x => x.GetCurrentSalary("Bar")).Returns(30);

			//Test setup
			SetScenarioRecording(false);
			Given.That<IDbAccess>(x => x.GetCurrentSalary("Foo")).Returns(40);
			Given.That<IDbAccess>(x => x.GetCurrentSalary("Foo")).Returns(50);

			Recorder.Transfer();

			Assert.AreEqual(40, stub.GetCurrentSalary("Bar"));
			Assert.AreEqual(50, stub.GetCurrentSalary("Foo"));
			Assert.AreEqual(50, stub.GetCurrentSalary("Foo"));
		}

		[TestMethod]
		public void FakeMustThrowIfScenarioIsNotRecording()
		{
			SetScenarioRecording(false);

			var exception = AssertEx.Throws<InvalidContextException>(() => A.Fake(new Fakes.Calculator()));

			Assert.AreEqual("A statements can only be used in an external scenario", exception.Message);
		}

		[TestMethod]
		public void FakeMustAddInteractionToDynamicDeclarations()
		{
			DynamicDeclarations.Reset();

			A.Fake(new Fakes.Calculator());

			Assert.AreEqual(1, DynamicDeclarations.GetDynamicInteractions().Length);
		}

		[TestMethod]
		public void FakeMustAddInteractionWithCalculatorTypeToDynamicDeclarations()
		{
			DynamicDeclarations.Reset();

			A.Fake(new Fakes.Calculator());

			Assert.AreEqual(DynamicDeclarations.GetDynamicInteractions()[0].RegistrationType, typeof(Fakes.Calculator));
		}

		[TestMethod]
		public void FakeMustThrowIfFakeInstanceIsNull()
		{
			var exception = AssertEx.Throws<ArgumentNullException>(() => A.Fake<Fakes.Calculator>(null));

			Assert.AreEqual("fakeInstance", exception.ParamName);
		}

		[TestMethod]
		public void FakeMustThrowIfTwoFakesOfSameTypeIsRegistered()
		{
			DynamicDeclarations.Reset();
			A.Fake(new Fakes.Calculator());

			var exception = AssertEx.Throws<InvalidContextException>(() => A.Fake(new Fakes.Calculator()));

			Assert.AreEqual("Fake for type Calculator has already been defined in scenario", exception.Message);
		}

		[TestMethod]
		public void FakeLateBound_ShouldThrow_WhenFieldNameIsNull()
		{
			var exception = AssertEx.Throws<ArgumentNullException>(() => A.FakeLateBound(null, 10));

			Assert.AreEqual("fieldName", exception.ParamName);
		}

		[TestMethod]
		public void FakeLateBound_ShouldThrow_WhenFieldNameIsEmptyString()
		{
			var exception = AssertEx.Throws<ArgumentNullException>(() => A.FakeLateBound(string.Empty, 10));

			Assert.AreEqual("fieldName", exception.ParamName);
		}

		[TestMethod]
		public void FakeLateBound_ShouldThrow_IfTwoFakesAreRegisteredOnSameField()
		{
			DynamicDeclarations.Reset();
			A.FakeLateBound("_fieldName", 10);

			var exception = AssertEx.Throws<InvalidContextException>(() => A.FakeLateBound("_fieldName", 10));

			Assert.AreEqual("Fake for field _fieldName has already been defined in scenario", exception.Message);
		}

		[TestMethod]
		public void FakeLateBound_ShouldAddDynamicRegistration_WhenFakeIsAdded()
		{
			DynamicDeclarations.Reset();

			A.FakeLateBound("_fieldName", 10);

			Assert.AreEqual(1, DynamicDeclarations.GetDynamicInteractions().Length);
		}

		[TestMethod]
		public void CallTo_MustThrow_WhenScenarioIsNotRecording()
		{
			SetScenarioRecording(false);

			var exception = AssertEx.Throws<InvalidContextException>(() => A.CallTo<IDbAccess, int>(x => x.GetCurrentSalary("Foo")).Returns(23));

			Assert.AreEqual("A statements can only be used in an external scenario", exception.Message);
		}

		private IList<IInvocationUpdater> GetRecordings()
		{
			FieldInfo fieldInfo = typeof(Recorder).GetField("_recordings", BindingFlags.NonPublic | BindingFlags.Static);
			return (IList<IInvocationUpdater>)fieldInfo.GetValue(null);
		}
	}
}
